using System;
using System.Collections.Generic;
using System.Threading;

namespace GerbilLib
{
    /// <summary>
    /// This class is an implementation of an asynchronous sortable binding list
    /// that can be used as a BindingSource for a databinding control such as
    /// the DataGridView control.
    /// The asynchronous functionality allows the list to be updated by threads
    /// other than the UI thread without causing a cross thread exception to be
    /// thrown on the control the list is bound to.
    /// </summary>
    /// <typeparam name="T">Type of the objects in the list</typeparam>
    public class AsyncBindingList<T> : SortableBindingList<T>
    {
        /// <summary>
        /// Creates a new Asynchronous Sortable Binding List.
        /// </summary>
        public AsyncBindingList()
            : base()
        {
            Initialize();
        }

        /// <summary>
        /// Creates a new AsyncBindingList on the UI thread.
        /// </summary>
        /// <param name="list">List to create the AsyncBindingList with</param>
        public AsyncBindingList(IList<T> list)
            : base(list)
        {
            Initialize();
        }

        /// <summary>
        /// Sets up the synchronization context and update callback.
        /// </summary>
        private void Initialize()
        {
            this.syncContext = new System.Windows.Forms.WindowsFormsSynchronizationContext();
            this.updateCallback = new SendOrPostCallback(this.UpdateBindingList);
        }

        #region .   Overrides from the base class. Executed on the Worker thread or on the UI thread   .
        /// <summary>
        /// Gets the number of elements actually contained in the AsyncBindingList.
        /// </summary>
        public new int Count
        {
            get
            {
                if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
                {
                    return base.Count;
                }
                else
                {
                    DataTransaction tr = new DataTransaction(DataOperationKind.GetCount, null, -1);
                    this.syncContext.Send(this.updateCallback, tr);
                    return (int)tr.TransactionResult;
                }
            }
        }

        /// <summary>
        /// Adds an object to the end of the AsyncBindingList.
        /// </summary>
        /// <param name="item">The object to be added to the end of the AsyncBindingList. The value can be null for reference types.</param>
        public void Add(T item, bool synchronousAdd = false)
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.Add(item);
            }
            else
            {
                if (synchronousAdd)
                    this.syncContext.Send(this.updateCallback, new DataTransaction(DataOperationKind.Add, item, -1));
                else
                    this.syncContext.Post(this.updateCallback, new DataTransaction(DataOperationKind.Add, item, -1));
            }
        }

        /// <summary>
        /// Adds a new item to the end of the collection.
        /// </summary>
        /// <returns></returns>
        protected override object AddNewCore()
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                return base.AddNewCore();
            }
            else
            {
                // we need the result for AddNewCore...
                // So we block until AddNewCore finishes on the UI thread.
                DataTransaction transaction = new DataTransaction(DataOperationKind.AddNewItem, null, -1);
                this.syncContext.Send(this.updateCallback, transaction);

                return transaction.TransactionResult;
            }
        }

        /// <summary>
        /// Removes all elements from the collection.
        /// </summary>
        protected override void ClearItems()
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.ClearItems();
            }
            else
            {
                this.syncContext.Post(this.updateCallback, new DataTransaction(DataOperationKind.Clear, null, -1));
            }
        }

        /// <summary>
        /// Inserts the specified item in the list at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index where the item is to be inserted.</param>
        /// <param name="item">The item to insert in the list.</param>
        protected override void InsertItem(int index, T item)
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.InsertItem(index, item);
            }
            else
            {
                DataTransaction tr =  new DataTransaction(DataOperationKind.InsertItem, item, index);
                this.syncContext.Post(this.updateCallback, tr);
            }
        }

        /// <summary>
        /// Removes the item at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index of the item to be removed.</param>
        protected override void RemoveItem(int index)
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.RemoveItem(index);
            }
            else
            {
                this.syncContext.Post(this.updateCallback, new DataTransaction(DataOperationKind.RemoveItem, null, index));
            }
        }

        /// <summary>
        /// Replaces the item at the specified index with the specified item.
        /// </summary>
        /// <param name="index">The zero-based index of the item to be replaced.</param>
        /// <param name="item">The new value for the item at the specified index. The value can be null for reference types.</param>
        protected override void SetItem(int index, T item)
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.SetItem(index, item);
            }
            else
            {
                this.syncContext.Post(this.updateCallback, new DataTransaction(DataOperationKind.SetItem, item, index));
            }
        }

        /// <summary>
        /// Discards a pending new item.
        /// </summary>
        /// <param name="itemIndex">The index of the new item to be added.</param>
        public override void CancelNew(int itemIndex)
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.CancelNew(itemIndex);
            }
            else
            {
                this.syncContext.Post(this.updateCallback, new DataTransaction(DataOperationKind.CancelNew, null, itemIndex));
            }
        }

        /// <summary>
        /// Commits a pending new item to the collection.
        /// </summary>
        /// <param name="itemIndex">The index of the new item to be added.</param>
        public override void EndNew(int itemIndex)
        {
            if (SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext)
            {
                base.EndNew(itemIndex);
            }
            else
            {
                this.syncContext.Post(this.updateCallback, new DataTransaction(DataOperationKind.EndNew, null, itemIndex));
            }
        }
        #endregion

        #region .   Private members for processing Posted messages. Used on the UI thread   .
        private SynchronizationContext syncContext = null;
        private SendOrPostCallback updateCallback = null;
        #endregion

        #region .   Process Posted messages. Executed on the UI thread   .
        /// <summary>
        /// Updates the binding list.
        /// </summary>
        /// <param name="arg">The DataTransaction object containing an update operation.</param>
        private void UpdateBindingList(object arg)
        {
            DataTransaction transaction = arg as DataTransaction;
            if (transaction == null)
            {
                return;
            }

            ExecuteTransaction(transaction);
        }

        /// <summary>
        /// Executes the given transaction on the binding list.
        /// </summary>
        /// <param name="transaction">The transaction to execute</param>
        private void ExecuteTransaction(DataTransaction transaction)
        {
            switch (transaction.DataOperationKind)
            {
                case DataOperationKind.Clear:
                    ClearItems();
                    break;
                case DataOperationKind.InsertItem:
                    InsertItem(transaction.Index, (T)transaction.DataObject);
                    break;
                case DataOperationKind.RemoveItem:
                    RemoveItem(transaction.Index);
                    break;
                case DataOperationKind.SetItem:
                    SetItem(transaction.Index, (T)transaction.DataObject);
                    break;
                case DataOperationKind.AddNewItem:
                    object res = AddNewCore();
                    transaction.TransactionResult = res;
                    break;
                case DataOperationKind.Add:
                    Add((T)transaction.DataObject);
                    break;
                case DataOperationKind.EndNew:
                    EndNew(transaction.Index);
                    break;
                case DataOperationKind.CancelNew:
                    CancelNew(transaction.Index);
                    break;
                case DataOperationKind.GetCount:
                    transaction.TransactionResult = Count;
                    break;
            }
        }
        #endregion
        
        #region .   Enums and classes for Posting   .
        /// <summary>
        /// List of operations that can be executed in a DataTransaction on the AsyncBindingList.
        /// </summary>
        private enum DataOperationKind
        {
            Clear,
            InsertItem,
            RemoveItem,
            SetItem,
            AddNewItem,
            Add,
            EndNew,
            CancelNew,
            GetCount,
            ApplySort,
            RemoveSort,
            Find
        }

        /// <summary>
        /// Internal class used for asynchronous transactions on the AsyncBindingList.
        /// </summary>
        private class DataTransaction
        {
            /// <summary>
            /// Gets or sets the kind of the data operation.
            /// </summary>
            /// <value>
            /// The kind of the data operation.
            /// </value>
            public DataOperationKind DataOperationKind { get; private set; }
            /// <summary>
            /// Gets or sets the data object.
            /// </summary>
            /// <value>
            /// The data object.
            /// </value>
            public Object DataObject { get; private set; }
            /// <summary>
            /// Gets or sets the index.
            /// </summary>
            /// <value>
            /// The index.
            /// </value>
            public int Index { get; private set; }
            /// <summary>
            /// Gets or sets the transaction result.
            /// </summary>
            /// <value>
            /// The transaction result.
            /// </value>
            public object TransactionResult { get; set; }

            /// <summary>
            /// Initializes a new instance of the <see cref="AsyncBindingList&lt;T&gt;.DataTransaction"/> class.
            /// </summary>
            /// <param name="kind">The kind of data operation.</param>
            /// <param name="obj">The data object.</param>
            /// <param name="index">The index.</param>
            public DataTransaction(DataOperationKind kind, object obj, int index)
            {
                DataOperationKind = kind;
                DataObject = obj;
                Index = index;
            }
        }
        #endregion
    }
}
